// Web sockets implementation

import std.concurrency, std.stdio, std.string, std.array, std.conv, std.md5, std.socket, std.json;

import Registry.registry;
import Definitions.msgCatalogue;
import Definitions.procNames;
//import Web.Server.ws.wsgui;

class WebSocket {

    auto handshake = false;
    string header;
    string accumulator;

    Socket sock;

    // CONSTRUCTOR
	this(Socket sock)
	{
        this.sock = sock;
        // Register the new socket as an event sink
        CSockets.register(sock);
	}

    public void feed(string data) {
        // Accepts data from the connection
        //writeln("Got data");
        if (!handshake) {
            //writeln("Doing handshake");
            // Concatenate the next chunk
            header = header ~ data;
            //writeln("Header: ", header);
            //writeln("Count: ", count(header, "\r\n\r\n"));
            if (count(header, "\r\n\r\n") > 0) {
                // We have a complete client handshake
                // Split the header and key parts
                auto parts = split(header, "\r\n\r\n");
                //writeln("Parts: ", parts[0], ", nonce: ", cast(ubyte[])parts[1]);
                if (do_handshake(header, cast(ubyte[])parts[1])) {
                    // Handshake was valid
                    //writeln("Valid handshake");
                    handshake = true;
                }
            }
        }
        else {
            // Have a valid connection
            // The message framing consists of
            // Frame types:
            // 0xFF - data frame
            // 0x00 - close frame
            // The frame ident is followed by the length of data in bytes
            // Length is big-endian 64 bits. Data is UTF8
            // The close frame is therefore 9 bytes of 0x00 (tag followed by length)
            // HOWEVER:
            // The current implementation of Chrome starts the message with 0x00 and there
            // seems to be no 0xFF anywhere in the message and no length. This partly
            // implements the draft 76 spec I think

            // Add the data to the current accumulation
            accumulator = accumulator ~ data;
            // In theory the message should terminate with 0xFF so split at that point
            auto messages = split(accumulator, "0xFF");
            // Update the accumulator with the start of a new message if there is one
            if (messages.length > 1) {
                accumulator = messages[1];
            }
            else {
                accumulator = "";
            }
            foreach(msg; messages) {
                if (msg[0] == '\x00') {
                    onmessage(msg[1 .. $]);
                }
            }
        }
    }

    private void onmessage(string message) {
        // Handle the message
        //writeln("Got message: ", message, message.length, cast(ubyte[])message);

        wsMessage(message);
    }

    private void wsMessage(string message) {
        // Handle the message
        //writeln("Got message: ", message, message.length, cast(ubyte[])message);

        // Event into the system
        string mode;
        string filter;
        string digit;
        JSONValue value;

        value = parseJSON(message);

        switch (value.object.keys()[0]) {
            case "mode":
                mode = value.object["mode"].str;
                switch(mode) {
                    case "lsb": mode = "LSB"; break;
                    case "usb": mode = "USB"; break;
                    case "cwl": mode = "CWL"; break;
                    case "cwu": mode = "CWU"; break;
                    case "am": mode = "AM"; break;
                    case "fmn": mode = "FMN"; break;
                    default: mode = "LSB"; break;
                }
                CRegistry.getTid(E_PROC.DSP).send(M_MODE(), mode);
                break;
            case "filter":
                filter = value.object["filter"].str;
                switch(filter) {
                    case "6k0": filter = "6K0"; break;
                    case "4k0": filter = "4K0"; break;
                    case "2k7": filter = "2K7"; break;
                    case "2k4": filter = "2K4"; break;
                    case "2k1": filter = "2K1"; break;
                    case "1k0": filter = "1K0"; break;
                    case "500": filter = "500"; break;
                    case "250": filter = "250"; break;
                    case "100": filter = "100"; break;
                    case "50": filter = "50"; break;
                    default: filter = "2K4"; break;
                }
                CRegistry.getTid(E_PROC.DSP).send(M_RX_FILT(), filter);
                break;
            case "freq":
                digit = value.object["freq"].str;
                //writeln("Digit: ", digit);
                double inc;
                switch(digit) {
                    case "100MHz": inc = 100.0; break;
                    case "10MHz": inc = 10.0; break;
                    case "1MHz": inc = 1.0; break;
                    case "100KHz": inc = 0.1; break;
                    case "10KHz": inc = 0.01; break;
                    case "1KHz": inc = 0.001; break;
                    case "100Hz": inc = 0.0001; break;
                    case "10Hz": inc = 0.00001; break;
                    case "1Hz": inc = 0.000001; break;
                    default: inc = 0.0; break;
                }
                auto delta = value.object["delta"].integer;
                //writeln("Delta: ", delta);
                if(delta < 0) {inc = -inc;}
                CRegistry.getTid(E_PROC.HPSDR).send(M_INC_FREQ(), inc);
                break;
        }
    }

    private void close() {
        // Close the connection

        writeln("Closing connection");
        //client.close();

    }

    private bool do_handshake(string header, ubyte[] key) {

        /*
            Creating the challenge response
            1. Look for the header lines Sec-WebSocket-Key1 and Sec-WebSocket-Key2
            2. Extract the digits from the key lines
            3. Count the spaces in the key lines
            4. Divide the digits by the spaces to get two parts to the response
            5. Pack these as follows (where key was passed in as the digest data at end of header):
                challenge = struct.pack('!I', part_1) + struct.pack('!I', part_2) + key
            6. Create the response as follows:
                response = hashlib.md5(challenge).digest()

            The header is of this form:
                GET / HTTP/1.1
                Upgrade: WebSocket
                Connection: Upgrade
                Host: localhost:9999
                Origin: http://localhost:9000
                Sec-WebSocket-Key1: 226+n17 43368
                Sec-WebSocket-Key2: l * 7z+   [7c0%0  {96880
                Cookie: session_id=691791f29e0d41d018ae5d6a1f5895b1f3158821
            with each line terminated with \n\r
        */

        // Handshake
        auto handshake = "HTTP/1.1 101 Web Socket Protocol Handshake\r
Upgrade: WebSocket\r
Connection: Upgrade\r
WebSocket-Origin: http://localhost:9000\r
WebSocket-Location: ws://localhost:9999/\r
Sec-Websocket-Origin: http://localhost:9000\r
Sec-Websocket-Location: ws://localhost:9999/\r\n\r\n";

        // Should these be int or uint?
        uint part_1;
        uint part_2;
        // Split the header into an array of lines
        auto lines = splitlines(header);
        foreach(line; lines) {
            if (line.length == 0) break;
            //writeln("Line: ", line, " ", line.length);
            // Split the line into a key and value pair
            // Only want the first split, how to do?
            auto pair = split(line, ":");
            uint numeric_part;
            uint spaces;
            if (tolower(pair[0]) == "sec-websocket-key1") {
                //writeln("Found key1 ", pair[0], pair[1], ".");
                // Found the first key
                // Extract the numerics in the key
                auto s = removechars(pair[1], "^0-9");
                //writeln("Numeric: ", s);
                numeric_part = parse!(uint)(s);
                //writeln("Numeric int: ", numeric_part);
                // Count the number of spaces (excluding the first)
                spaces = countchars(pair[1], " ")-1;
                //writeln("Spaces: ", spaces);
                // Create the first parts of the response
                //writeln("Remainder: ",numeric_part%spaces);
                part_1 = numeric_part/spaces;
                //writeln("Part 1: ", part_1);
            }
            else if (tolower(pair[0]) == "sec-websocket-key2") {
                //writeln("Found key2 ", pair[0], pair[1], ".");
                // Found the second key
                // Extract the numerics in the key
                auto s = removechars(pair[1], "^0-9");
                //writeln("Numeric: ", s);
                numeric_part = parse!(uint)(s);
                //writeln("Numeric int: ", numeric_part);
                // Count the number of spaces (excluding the first)
                spaces = countchars(pair[1], " ")-1;
                //writeln("Spaces: ", spaces);
                // Create the second parts of the response
                //writeln("Remainder: ",numeric_part%spaces);
                part_2 = numeric_part/spaces;
                //writeln("Part 2: ", part_2);
            }
        }

        // Create the challenge response
        // The response is formed by a concatenation of
        // part_1 + part_2 + key (where parts are 32 bit integers and key is 8 bytes)
        // The parts must be in big endian order and the whole forms a 128 bit value
        // The response is then the MD5 hash of this value
        //writeln("Creating response");
        auto response = new ubyte[16];
        /*
        response[0] = part_1 & 0xFF;
        response[1] = (part_1 >> 8) & 0xFF;
        response[2] = (part_1 >> 16) & 0xFF;
        response[3] = (part_1 >> 24) & 0xFF;
        response[4] = part_2 & 0xFF;
        response[5] = (part_2 >> 8) & 0xFF;
        response[6] = (part_2 >> 16) & 0xFF;
        response[7] = (part_2 >> 24) & 0xFF;
        */
        // It appears not to want big endian!!!
        response[3] = part_1 & 0xFF;
        response[2] = (part_1 >> 8) & 0xFF;
        response[1] = (part_1 >> 16) & 0xFF;
        response[0] = (part_1 >> 24) & 0xFF;
        response[7] = part_2 & 0xFF;
        response[6] = (part_2 >> 8) & 0xFF;
        response[5] = (part_2 >> 16) & 0xFF;
        response[4] = (part_2 >> 24) & 0xFF;
        foreach(i, value; key) {
            response[i+8] = value;
        }

        //writeln("Response: ", response);
        // Create an MD5 hash of the result.
        ubyte digest[16];
        sum(digest, response);

        // Send the handshake
        //writeln("Handshake sending: ", cast(ubyte[])handshake ~ digest);
        this.sock.send(cast(ubyte[])handshake ~ digest);

        return true;
    }
}

